﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text.RegularExpressions;
using Scrabble.Model.Events;
using Scrabble.Model.Properties;

namespace Scrabble.Model
{
    public partial class Game
    {
        class Player : IPlayer, INotifyPropertyChanged
        {
            private readonly List<Tile> _rack = new List<Tile>();

            public event PropertyChangedEventHandler PropertyChanged = delegate { };

            public Player(Game game, string name)
            {
                Contract.Requires<ArgumentNullException>(game != null);
                Contract.Requires<ArgumentException>(!String.IsNullOrWhiteSpace(name));

                Name = name;
                Game = game;

                Game.FillRack(_rack);
                Game.ActivePlayerChanged += delegate { NotifyOfPropertyChanged("IsActive"); };
                Game.GameOver += delegate { OnGameOver(); };
            }

            private Game Game
            {
                get;
                set;
            }

            public string Name
            {
                get;
                private set;
            }

            public bool IsActive
            {
                get { return Game.ActivePlayer == this; }
            }

            public int CurrentScore
            {
                get { return EventLog.Aggregate(0, (result, action) => result + action.Score); }
            }

            public TimeSpan TimeInGame
            {
                get { return EventLog.Aggregate(TimeSpan.Zero, (result, action) => result + action.DurationOfPlay); }
            }

            public IEnumerable<Tile> CurrentTiles
            {
                get { return _rack; }
            }

            public void Pass()
            {
                GuardAgainstPlayingOutOfTurn();
                CompletePlay(new SkippedTurnEvent(this));
            }

            public void PlaceTilesFromNotation(string info)
            {
                Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(info));

                var groups = Regex.Match(info, @"(?<location>([A-O]\d+|\d+[A-O])) (\((?<prefix>\w+)\))?(?<play>[\(\)\w]+)").Groups;

                var vector = PlayVector.Parse(groups["location"].Value);

                if (groups["prefix"].Success)
                    vector.Skip(groups["prefix"].Value.Length);

                var letters = Regex.Replace(groups["play"].Value, @"\(\w+?\)", String.Empty);

                PlaceTiles(TilesFromLetters(letters).ToArray(), vector);
            }

            public void PlaceTiles(IEnumerable<Tile> tiles, PlayVector vector)
            {
                Guard.Against<InvalidOperationException>(!HasTiles(tiles), "Error: attempting to play tiles that the player doesn't have");
                Guard.Against<InvalidOperationException>(Game.Board.TileExistsAt(vector.StartingLocation), "Error: play can only be made at an empty location");

                GuardAgainstPlayingOutOfTurn();

                var totalTiles = tiles.Count();
                var placementLocations = Game.Board.AvailableLocationsIn(vector).Take(totalTiles);

                Guard.Against<InvalidMoveException>(placementLocations.Count() < totalTiles, Resources.Prompt_InsufficientSpaceOnBoard);

                if (Game.Board.IsEmpty)
                {
                    Guard.Against<InvalidMoveException>(!placementLocations.Any(l => l == Board.CenterLocation), Resources.Prompt_IllegalFirstMove);
                }
                else
                {
                    var touchedLocations = placementLocations
                        .SelectMany(location => location.NeighbouringLocations)
                        .Any(neighbouringLocation => Game.Board.TileExistsAt(neighbouringLocation));

                    Guard.Against<InvalidMoveException>(!touchedLocations, Resources.Prompt_PlayMustTouchOtherTiles);
                }

                var scoreForPlay = Game.ScorePlacement(tiles, vector);
                var recordOfPlay = new PlayedWordEvent(this, Game.SetOfWordsFormedBy(tiles, vector).First(), scoreForPlay, tiles);

                // ReSharper disable AssignNullToNotNullAttribute
                Game.PlaceTiles(_rack, placementLocations.Zip(tiles, (location, tile) => new TileInPlay(tile, location)));
                // ReSharper restore AssignNullToNotNullAttribute
                CompletePlay(recordOfPlay);

                NotifyOfPropertyChanged("CurrentScore");
                NotifyOfPropertyChanged("CurrentTiles");
            }

            public IEnumerable<Tile> TilesFromLetters(IEnumerable<char> letters)
            {
                Contract.Requires<ArgumentNullException>(letters != null);

                var availableTiles = new List<Tile>(CurrentTiles);

                foreach (var letter in letters)
                {
                    var testLetter = letter;

                    var matchingTile = Char.IsLower(testLetter)
                        ? availableTiles.FirstOrDefault(tile => tile.InitiallyBlank)
                        : availableTiles.FirstOrDefault(tile => tile.Letter == testLetter);

                    if (matchingTile.InitiallyBlank)
                        matchingTile.UseBlankAs(letter);

                    availableTiles.Remove(matchingTile);
                    yield return matchingTile;
                }
            }

            public void ExchangeTiles(Tile[] tiles)
            {
                GuardAgainstPlayingOutOfTurn();

                Game.ExchangeTiles(_rack, tiles);
                CompletePlay(new TilesExchangedEvent(this, tiles));

                NotifyOfPropertyChanged("CurrentTiles");
            }

            [Pure]
            public bool HasTiles(IEnumerable<Tile> tiles)
            {
                Contract.Requires<ArgumentNullException>(tiles != null);
                return _rack.Intersect(tiles).Count() == tiles.Count();
            }

            private IEnumerable<IGameEvent> EventLog
            {
                get { return Game._eventLog.Where(action => action.Player == this); }
            }

            private void GuardAgainstPlayingOutOfTurn()
            {
                Guard.Against<InvalidMoveException>(!IsActive, Game.ActivePlayer == null
                    ? "The game is finished - no moves are possible."
                    : String.Format("Only {0} can complete a move at this time.", Game.ActivePlayer.Name));
            }

            private void CompletePlay(GameEvent recordOfPlay)
            {
                Game.GoToNextPlayer(recordOfPlay);
                NotifyOfPropertyChanged("TimeInGame");
                NotifyOfPropertyChanged("History");
            }

            private void OnGameOver()
            {
                NotifyOfPropertyChanged("CurrentScore");
                NotifyOfPropertyChanged("History");
            }

            private void NotifyOfPropertyChanged(string propertyName)
            {
                var handler = PropertyChanged;
                if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
